package cn.tedu.charging.cost.service.impl;

import cn.tedu.charging.common.pojo.param.ChargingProcessParam;
import cn.tedu.charging.common.pojo.vo.ChargingProcessVO;
import cn.tedu.charging.cost.dao.repository.ChargingDataESRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleCacheRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleRepository;
import cn.tedu.charging.cost.pojo.ChargingData;
import cn.tedu.charging.cost.pojo.po.ChargingDataPO;
import cn.tedu.charging.cost.pojo.po.CostRulePO;
import cn.tedu.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;

@Slf4j
@Service
public class CostServiceImpl implements CostService {

    @Autowired
    private CostRuleRepository costRuleRepository;

    /**
     * 全局变量Map 用来记录充电数据
     * Key :订单号
     * Value 充电数据 实际的充电数据（次数，度数，花费...）
     *
     * 本地变量Map问题
     * >>>>>1 数据丢失问题
     *  在内存里，如果服务重启 数据丢失 用来累加的数据也丢失了
     *
     * >>>>>2 数据量问题
     *  计价服务 部署到了线上的服务器
     *  一直运行，不重启，运行了一个月，100000个订单，每个订单平均充满需要2个小时
     *  数据每30s 同步一次 数据量很大 容易导致内存溢出
     *
     * >>>>>3 线程安全问题
     *  HashMap<> 不是线程安全的Map 应该使用线程安全的 ConcurrentHashMap
     *
     * >>>>>4 分布式问题
     * 计价服务 部署了 3台
     * 数据是独享的
     * 4040 本地的 Map
     * 4041 本地的 Map
     * 4042 本地的 Map
     * 设备同步数据到订单服务，订单服务访问通过 负载均衡 访问 计价服务
     * 第一次同步 4040  真实的第一次
     * 第二次同步 4041  对于4041来说 是第一次，对整个订单不是第一次 虚假的第一次
     *
     * 怎么解决 上面的 4个问题 ？
     *
     *
     *
     * 存储到其他的介质 比如 数据库
     * 用哪个数据库？
     * 计价服务的数据特点 对数据库的要求
     *  计价服务要存储大量的数据，数据量很大，为什么量大，是因为大量的设备在同时同步数据
     *  数据库 符合
     *  1 能存储 海量数据
     *  2 高性能 能很快存和取
     *  3 高可用 一直能高性能的存和取
     *  https://blog.csdn.net/qq_33034733/article/details/140869494 3高
     *
     *  Mysql 关系型数据库
     *  不是高性能 严格的ACID 性能相对较差 基于磁盘存储的
     *  不是海量数据 数据量少 单机Mysql的表数据存储的大小是有规定 如果太大性能就差
     *  是高可用 Mysql集群架构
     *
     *  Redis
     *  高性能 基于内存 内存数据库
     *  不是海量数据 内存贵 内存的容量有限 持久化 会导致性能降低
     *  是高可用 集群架构 哨兵 Cluster
     *
     * ES
     *  高性能
     *  海量数据
     *  高可用
     *
     * 关系型   Mysql Sqlserver Oracle
     * 非关系 Redis  K V
     * 非关系  MongoDB doc文档
     * 非关系  ES   doc 文档
     *
     *
     */
    private Map<String,ChargingData> chargingDataMap = new HashMap<>();


    @Override
    public ChargingProcessVO calculateCost(ChargingProcessParam chargingProcessParam) {
        //通过订单号获取充电数据 用来区分是第一次同步 还是第N次同步
        //ChargingData chargingData = getChargingDataByOrderNo(chargingProcessParam.getOrderNo());

        //把从本地map中获取充电数据修改为 从ES 获取充电数据
        ChargingDataPO chargingData =
                getChargingDataByOrderNoFromES(chargingProcessParam.getOrderNo(),
                        chargingProcessParam.getUserId(),
                        chargingProcessParam.getGunId());
        log.debug("通过订单号:{}," +
                        "获取充电数据:{}," +
                        "当前同步次数:{}",
                chargingProcessParam.getOrderNo(),
                chargingData,
                chargingData.getCount());
        //获取当前时间 小时
        Integer currentHour = getCurrentHour();
        log.debug("获取当前时间小时:{}",currentHour);
        //通过枪类型和当前时间获取匹配的计价规则
        CostRulePO costRulePO =
                getMatchCostRuleByGunTypeAndTime(chargingProcessParam.getStationId(),
                        chargingProcessParam.getGunType(),currentHour);
        log.debug("通过枪类型:{},时间:{},获取计价规则:{}",chargingProcessParam.getGunType(),currentHour,costRulePO);
        //判断计价规则是否为空
        if (costRulePO != null) {
            //获取充电度数
            Float chargingCapacity = chargingProcessParam.getChargingCapacity();
            //获取计价规则名称
            String name = costRulePO.getName();
            //获取电费 每度电的单价
            BigDecimal powerFee = costRulePO.getPowerFee();
            //判断是否是第一次同步数据
            //判断的原因
            // 第一次同步数据 可以直接 通过度数 * 电费 计算价格
            // 第N次同步数据 需要通过度数 - 上次同步的度数 计算真实充电度数 * 电费 计算价格
            //if (chargingData.getCount() == 1) {
            if (chargingData.isFirst()) {
                //第一次同步数据
                firstCost2ChargingData(chargingCapacity,
                        chargingData,currentHour,name, powerFee);
            }else {
                //第N次同步数据
                lastCost2ChargingData(chargingCapacity,
                        chargingData,currentHour,name, powerFee);
            }
        }else {
            log.error("故障，计价规则为空，要进行系统告警，业务无法继续执行");
        }
        //todo
        // 3 计价规则会高频的从mysql查询   缓存Redis 用Redis什么类型来存？
        ChargingProcessVO chargingProcessVO = buildChargingProcessVO(chargingData);
        return chargingProcessVO;
    }

    /**
     * 通过充电数据 构建 计价服务计算价格的出参
     * @param chargingData
     * @return
     */
    private ChargingProcessVO buildChargingProcessVO(ChargingDataPO chargingData) {
        ChargingProcessVO chargingProcessVO = new ChargingProcessVO();

        chargingProcessVO.setOrderNo(chargingData.getOrderNo());
        chargingProcessVO.setGunId(chargingData.getGunId());
        chargingProcessVO.setTotal(chargingData.getTotal());
        chargingProcessVO.setChargingCapacity(chargingData.getChargingCapacity());
        //如果参数过多，可以通过BeanUtils工具类进行拷贝
        //为啥能copy 属性值
        //BeanUtils.copyProperties 原理
        //1 获取源 source chargingData.class 获取getter readMethod 读方法
        //2 获取目标 target ChargingProcessVO.class 获取setter writeMethod 写方法
        //3 循环遍历 readMethod 读方法
        //4 判断读方法和写方法的访问修饰符 如果不是 public setAccessible(true) 强制访问
        //5 通过 source的读方法 读取source的值 Object value = readMethod.invoke(source);
        //6 通过 writeMethod 写方法 把值写入到target中 writeMethod.invoke(target, value);
        //属性名 必须一致才能拷贝 属性类型 会自动转换 能转换成功
        log.debug("BeanUtils.copyProperties 复制参数:source:{},target:{}",
                chargingData,chargingProcessVO);
        BeanUtils.copyProperties(chargingData,chargingProcessVO);
        log.debug("BeanUtils.copyProperties 复制参数:source:{},target:{}",
                chargingData,chargingProcessVO);
        //充电总时长的计算  充电的结束时间 - 充电的开始时间
        //结束时间 在充满后 设备告诉订单 充满的时间 是结束时间
        //开始时间 设备返回 开始充电响应结果的时间 存储在订单成功表 ,为了避免订单传输这个时间
        //开始时间采用的是 设备第一次同步充电数据的时间
        //每次同步充电数据 都应该给用户返回 已经充了多长时间
        //设备第一次同步充电数据的时间
        Long startTime = chargingData.getStartTime();
        //获取当前时间
        long now = System.currentTimeMillis();
        //消耗的时间
        //NPE null.方法
        Long costTime = now - startTime;
        //如何计算已经充了几个小时呢？
        //Date 不推荐使用  --> joda-time --> java.time LocalDateTime
        //LocalDateTime now  = LocalDateTime.now();
        //Duration between = Duration.between(, now);
        //between.toHours();//计算已经充了多少小时
        chargingProcessVO.setTotalTime(costTime);
        return chargingProcessVO;
    }


    /**
     * 计算第一次价格到充电数据
     * @param chargingCapacity 充电度数 包含关系
     * @param chargingData  充电数据 为计算真实充电度数 和 花费 做准备
     * @param currentHour  当前时间 小时
     * @param name  计价规则的名称
     * @param powerFee 计价规则的电费单价
     */
    private void firstCost2ChargingData(Float chargingCapacity,ChargingDataPO chargingData,
                                        Integer currentHour, String name,BigDecimal powerFee){
        log.debug("第一次同步数据，计算价格");
        //计算价格 充电度数 * 计价规则的每度电的价格
        BigDecimal cost = getCost(chargingCapacity, powerFee);
        log.debug("当前时间:{},计价规则的名称:{}," +
                        "充电度数:{},电费单价:{}," +
                        "本次花费:{},总花费:{}",
                currentHour,name,
                chargingCapacity,powerFee,
                cost,cost);
        //保存当前充电度数到充电数据，为下次 <<<计算真实充电度数>>>做准备
        //当前充电度数 - 上次充电度数 = 实际充电度数  减
        chargingData.setChargingCapacity(chargingCapacity);
        //保存当前花费到充电数据，为下次 <<<计算真实花费>>>做准备
        //当前花费 + 上次花费 = 总花费    加
        chargingData.setTotal(cost);
        //保存修改后的充电数据到ES
        log.debug("第一次同步充电数据，计算价格后，并且保存到ES:{}",chargingData);
        chargingDataESRepository.save(chargingData);
        log.debug("第一次同步充电数据，计算价格后，并且保存到ES成功{}",chargingData);

    }

    /**
     * 不是第一次计算价格
     * @param chargingCapacity 充电度数 包含上次的度数
     * @param chargingData 充电数据 为计算真实充电度数 和 花费 做准备
     * @param currentHour 当前时间 小时
     * @param name  计价规则的名称
     * @param powerFee 计价规则的电费单价
     */
    private void lastCost2ChargingData(Float chargingCapacity,ChargingDataPO chargingData,
                                       Integer currentHour, String name,BigDecimal powerFee){

        log.debug("不是第一次同步数据，计算价格");
        //上次同步的充电度数
        Float realChargingCapacity = chargingData.getChargingCapacity(chargingCapacity);
        //实际充电度数 =  当前同步的充电度数 - 上次保存的充电度数
        //Float realChargingCapacity =  chargingCapacity - lastChargingCapacity;
        //计算价格 真实充电度数 * 电费单价
        BigDecimal currentCost = getCost(realChargingCapacity, powerFee);
        //保存当前花费到充电数据，为下次 <<<计算真实花费>>>做准备
        chargingData.setTotal(currentCost);
        //上次的花费
        //BigDecimal lastCost = chargingData.getTotal();
        //当前总话费 = 上次花费 + 本次花费
        //BigDecimal total = lastCost.add(cost);
        log.debug("当前时间:{},计价规则的名称:{}," +
                        "充电度数:{},电费单价:{}," +
                        "本次花费:{},总花费:{}",
                currentHour,name,
                realChargingCapacity,powerFee,
                currentCost,chargingData.getTotal());
        //保存当前充电度数到充电数据，为下次 <<<计算真实充电度数>>>做准备
        chargingData.setChargingCapacity(chargingCapacity);
        //保存修改后的充电数据到ES
        log.debug("第{}次同步充电数据，计算价格后，并且保存到ES:{}",chargingData.getCount(),chargingData);
        chargingDataESRepository.save(chargingData);
        log.debug("第{}次同步充电数据，计算价格后，并且保存到ES成功{}",chargingData.getCount(),chargingData);

    }

    @Autowired
    private ChargingDataESRepository chargingDataESRepository;

    /**
     * 通过订单号获取 充电数据
     * 将充电数据存储到 ES 用来解决本地map的问题
     * @param orderNo
     * @return
     */
    private ChargingDataPO getChargingDataByOrderNoFromES(String orderNo,Integer userId,Integer gunId){
        //通过订单号去ES里查询充电数据
        Optional<ChargingDataPO> optional
                = chargingDataESRepository.findById(orderNo);
        //判断 optional 是否有数据 value != null;
        if (optional.isPresent()) {
            ChargingDataPO chargingDataPO = optional.get();
            //第N次
            //取上次的次数
            Integer count = chargingDataPO.getCount();
            //上次的count + 本次的count
            count = count + 1;
            chargingDataPO.setCount(count);
            //修改后的数据保存到ES
            log.debug("第{}次同步充电数据，修改后的数据保存到ES:{}",count,chargingDataPO);
            chargingDataESRepository.save(chargingDataPO);
            log.debug("第{}次同步充电数据，修改后的数据保存到ES 成功:{}",count,chargingDataPO);
            return chargingDataPO;
        }else {
            //第一次
            //从ES没有查询到充电数据 表示第一次同步
            //初始化一个充电数据
            ChargingDataPO chargingDataPO = new ChargingDataPO();
            chargingDataPO.setCount(1);
            chargingDataPO.setOrderNo(orderNo);
            chargingDataPO.setGunId(gunId);
            chargingDataPO.setId(orderNo);
            chargingDataPO.setUserId(userId);
            chargingDataPO.setStartTime(System.currentTimeMillis());
            //修改后的数据保存到ES
            log.debug("第一次同步充电数据，初始化充电数据，并且保存到ES:{}",chargingDataPO);
            chargingDataESRepository.save(chargingDataPO);
            log.debug("第一次同步充电数据，初始化充电数据，并且保存到ES 成功:{}",chargingDataPO);
            return chargingDataPO;
        }
    }


    /**
     * 通过订单号获取 充电数据
     * @param orderNo
     * @return
     */
    private ChargingData getChargingDataByOrderNo(String orderNo) {
        //本地变量不行 每次都需要创建 应该用全局变量
        //Map<String,ChargingData> chargingDataMap = new HashMap<>();
        ChargingData chargingData = chargingDataMap.get(orderNo);
        if (chargingData == null) {
            //第一次
            chargingData = new ChargingData();
            chargingData.setCount(1);
            chargingDataMap.put(orderNo,chargingData);
        }else {
            //第N次
            //取上次的次数
            Integer count = chargingData.getCount();
            //上次的count + 本次的count
            count = count + 1;
            chargingData.setCount(count);
            //todo  不用 把修改后的chargingData 存储到map中
        }
        return chargingData;
    }

    /**
     * 计算费用 充电度数 * 计价规则的每度电的价格
     * @param chargingCapacity
     * @param powerFee
     * @return
     */
    private BigDecimal getCost(Float chargingCapacity,BigDecimal powerFee) {
        //通过构造器 进行类型转换
        //Float 转换为 BigDecimal
        //BigDecimal bigDecimal = new BigDecimal(chargingCapacity);
        //基本类型的数字转换为 包装类型的 Integer
        //Integer i = new Integer(1);
        //String 转换为 包装类型的 String
        //String s = new String("1");
        //通过方法 进行转换 valueOf
        //通过方法 valueOf 转换 float 转为 String
        //String s = String.valueOf(chargingCapacity);
        //Float 转换为 BigDecimal
        BigDecimal bigDecimal = BigDecimal.valueOf(chargingCapacity);
        //BigDecimal 好用的计算 add 加法 multiply 乘法
        BigDecimal multiply = powerFee.multiply(bigDecimal);
        return multiply;
    }


    /**
     * 获取当前时间 小时
     * @return
     */
    private Integer getCurrentHour(){
        long l = System.currentTimeMillis();
        //Date date = new Date();
        //@Deprecated
        //date.getDate()
        //LocalDateTime 比 Date 更新 更好用
        LocalDateTime now = LocalDateTime.now();
        return now.getHour();
    }


    /**
     * 通过时间和枪类型 去计价规则列表匹配一条计价规则
     * @param stationId 站id
     * @param gunType 枪类型
     * @param hour 时间
     * @return
     */
    private CostRulePO getMatchCostRuleByGunTypeAndTime(Integer stationId,
                                                        Integer gunType,
                                                        Integer hour){
        long start = System.currentTimeMillis();
        log.debug("通过站id:{},获取计价规则列表,开始时间:{}",stationId,start);
        List<CostRulePO> costRules = getCostRule(stationId);
        long end = System.currentTimeMillis();
        long costTime = end - start;
        log.debug("通过站id:{},获取计价规则列表:{},开始时间:{},结束时间:{},消耗时间:{}",
                stationId,costRules,start,end,costTime);
        if (!CollectionUtils.isEmpty(costRules)) {
            for (CostRulePO costRule : costRules) {
                //获取到计价规则的枪类型
                Integer costRuleGunType = costRule.getGunType();
                //判断枪类型是否匹配
                if (costRuleGunType.equals(gunType)) {
                    //枪类型匹配 开始匹配时间
                    //获取到计价规则的开始时间
                    Integer startTime = costRule.getStartTime();
                    //获取到计价规则的结束时间
                    Integer endTime = costRule.getEndTime();
                    /**
                     *  当前时间 11:34 获取小时 11
                     *  找到真实的计价规则 通过人肉的方式 去判断 当前时间属于哪个时间段
                     *    8-12
                     *   14-18
                     *   18-25
                     *  真实的计价规则
                     *
                     *  11 属于 8-12
                     *  11 > 8 并且 11 < 12
                     *  hour > startTime && hour < endTime
                     *  边界值的处理
                     *  如果当前时间 8 如何处理  ？
                     *  如果当前时间 12 如何处理 ？
                     *  如果当前时间 8 大于 还是 大于等于  ？
                     *  如果当前时间 12 小于 还是 小于等于 ？

                     *  hour >= startTime && hour < endTime
                     */
                    if (hour >= startTime && hour < endTime) {
                        return costRule;
                    }
                }
            }
        }
        //都没有匹配到，会跳出循环，表示没有匹配到计价规则
        return null;
    }

    @Autowired
    private CostRuleCacheRepository costRuleCacheRepository;

    /**
     * 1. 从Redis查询
     * 2. 判断Redis是否有数据
     * 3. 有 直接返回
     * 4. 没有 从数据库查
     * 5. 判断数据库是否有数据
     * 6. 有 写入Redis 并返回
     * 7. 没有 报错告警
     *
     * 旁路缓存
     *
     * 通过场站id获取计价规则列表
     * @param stationId
     * @return
     */
    private List<CostRulePO> getCostRule(Integer stationId){
        log.debug("通过场站id:{},从Redis查询计价规则",stationId);
        List<CostRulePO> cacheCostRules =
                costRuleCacheRepository.getCostRuleByStationId(stationId);
        log.debug("通过场站id:{},从Redis查询计价规则列表:{}",stationId,cacheCostRules);
        //判断Redis查询出的计价规则是否为空
        if (!CollectionUtils.isEmpty(cacheCostRules)) {
            log.debug("通过场站id:{},从Redis查询计价规则列表，Redis有数据，直接返回",
                    stationId,cacheCostRules);
            return cacheCostRules;
        }else {
            log.debug("通过场站id:{},从Redis查询计价规则列表，Redis没有数据，从数据库查询",
                    stationId);
            List<CostRulePO> dbCostRules =
                    costRuleRepository.getCostRuleByStationId(stationId);
            log.debug("通过场站id:{},从Redis查询计价规则列表，Redis没有数据，从数据库查询计价规则列表:{}",
                    stationId,dbCostRules);
            //判断数据库查询出的计价规则是否为空
            if (!CollectionUtils.isEmpty(dbCostRules)) {
                log.debug("通过场站id:{},从数据库查询计价规则列表:{}，数据库有数据，写入Redis,并返回",
                        stationId,dbCostRules);
                costRuleCacheRepository.saveCostRules(stationId,dbCostRules);
                return dbCostRules;
            }
        }
        //return new ArrayList<>();
        return Collections.emptyList();
    }
}
